package com.baidu.iit.pxp.el;

import com.baidu.iit.pxp.el.juel.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.util.EnumSet;
import java.util.Properties;

/**
 * User: huangweili
 * Date: 14-4-29
 * Time: 下午2:02
 */
public class ExpressionFactoryImpl extends ExpressionFactory {

    public static enum Profile {

        JEE5(EnumSet.noneOf(Builder.Feature.class)),

        JEE6(EnumSet.of(Builder.Feature.METHOD_INVOCATIONS, Builder.Feature.VARARGS));

        private final EnumSet<Builder.Feature> features;

        private Profile(EnumSet<Builder.Feature> features) {
            this.features = features;
        }

        Builder.Feature[] features() {
            return features.toArray(new Builder.Feature[features.size()]);
        }

        boolean contains(Builder.Feature feature) {
            return features.contains(feature);
        }
    }


    public static final String PROP_METHOD_INVOCATIONS = "javax.el.methodInvocations";


    public static final String PROP_VAR_ARGS = "javax.el.varArgs";


    public static final String PROP_NULL_PROPERTIES = "javax.el.nullProperties";

    /**
     * <code>javax.el.cacheSize</code>
     */
    public static final String PROP_CACHE_SIZE = "javax.el.cacheSize";

    private final TreeStore store;
    private final TypeConverter converter;


    public ExpressionFactoryImpl() {
        this(Profile.JEE6);
    }


    public ExpressionFactoryImpl(Profile profile) {
        Properties properties = loadProperties("el.properties");
        this.store = createTreeStore(1000, profile, properties);
        this.converter = createTypeConverter(properties);
    }


    public ExpressionFactoryImpl(Properties properties) {
        this(Profile.JEE6, properties);
    }


    public ExpressionFactoryImpl(Profile profile, Properties properties) {
        this.store = createTreeStore(1000, profile, properties);
        this.converter = createTypeConverter(properties);
    }

    public ExpressionFactoryImpl(Properties properties, TypeConverter converter) {
        this(Profile.JEE6, properties, converter);
    }


    public ExpressionFactoryImpl(Profile profile, Properties properties, TypeConverter converter) {
        this.store = createTreeStore(1000, profile, properties);
        this.converter = converter;
    }


    public ExpressionFactoryImpl(TreeStore store) {
        this(store, TypeConverter.DEFAULT);
    }


    public ExpressionFactoryImpl(TreeStore store, TypeConverter converter) {
        this.store = store;
        this.converter = converter;
    }

    private Properties loadDefaultProperties() {
        String home = System.getProperty("java.home");
        String path = home + File.separator + "lib" + File.separator + "el.properties";
        File file = new File(path);
        if (file.exists()) {
            Properties properties = new Properties();
            InputStream input = null;
            try {
                properties.load(input = new FileInputStream(file));
            } catch (IOException e) {
                throw new ELException("Cannot read default EL properties", e);
            } finally {
                try {
                    input.close();
                } catch (IOException e) {
                    // ignore...
                }
            }
            if (getClass().getName().equals(properties.getProperty("javax.el.ExpressionFactory"))) {
                return properties;
            }
        }
        if (getClass().getName().equals(System.getProperty("javax.el.ExpressionFactory"))) {
            return System.getProperties();
        }
        return null;
    }

    private Properties loadProperties(String path) {
        Properties properties = new Properties(loadDefaultProperties());

        // try to find and load properties
        InputStream input = null;
        try {
            input = Thread.currentThread().getContextClassLoader().getResourceAsStream(path);
        } catch (SecurityException e) {
            input = ClassLoader.getSystemResourceAsStream(path);
        }
        if (input != null) {
            try {
                properties.load(input);
            } catch (IOException e) {
                throw new ELException("Cannot read EL properties", e);
            } finally {
                try {
                    input.close();
                } catch (IOException e) {
                    // ignore...
                }
            }
        }

        return properties;
    }

    private boolean getFeatureProperty(Profile profile, Properties properties, Builder.Feature feature, String property) {
        return Boolean.valueOf(properties.getProperty(property, String.valueOf(profile.contains(feature))));
    }


    protected TreeStore createTreeStore(int defaultCacheSize, Profile profile, Properties properties) {
        // create builder
        TreeBuilder builder = null;
        if (properties == null) {
            builder = createTreeBuilder(null, profile.features());
        } else {
            EnumSet<Builder.Feature> features = EnumSet.noneOf(Builder.Feature.class);
            if (getFeatureProperty(profile, properties, Builder.Feature.METHOD_INVOCATIONS, PROP_METHOD_INVOCATIONS)) {
                features.add(Builder.Feature.METHOD_INVOCATIONS);
            }
            if (getFeatureProperty(profile, properties, Builder.Feature.VARARGS, PROP_VAR_ARGS)) {
                features.add(Builder.Feature.VARARGS);
            }
            if (getFeatureProperty(profile, properties, Builder.Feature.NULL_PROPERTIES, PROP_NULL_PROPERTIES)) {
                features.add(Builder.Feature.NULL_PROPERTIES);
            }
            builder = createTreeBuilder(properties, features.toArray(new Builder.Feature[0]));
        }

        // create cache
        int cacheSize = defaultCacheSize;
        if (properties != null && properties.containsKey(PROP_CACHE_SIZE)) {
            try {
                cacheSize = Integer.parseInt(properties.getProperty(PROP_CACHE_SIZE));
            } catch (NumberFormatException e) {
                throw new ELException("Cannot parse EL property " + PROP_CACHE_SIZE, e);
            }
        }
        Cache cache = cacheSize > 0 ? new Cache(cacheSize) : null;

        return new TreeStore(builder, cache);
    }


    protected TypeConverter createTypeConverter(Properties properties) {
        Class<?> clazz = load(TypeConverter.class, properties);
        if (clazz == null) {
            return TypeConverter.DEFAULT;
        }
        try {
            return TypeConverter.class.cast(clazz.newInstance());
        } catch (Exception e) {
            throw new ELException("TypeConverter " + clazz + " could not be instantiated", e);
        }
    }


    protected TreeBuilder createTreeBuilder(Properties properties, Builder.Feature... features) {
        Class<?> clazz = load(TreeBuilder.class, properties);
        if (clazz == null) {
            return new Builder(features);
        }
        try {
            if (Builder.class.isAssignableFrom(clazz)) {
                Constructor<?> constructor = clazz.getConstructor(Builder.Feature[].class);
                if (constructor == null) {
                    if (features == null || features.length == 0) {
                        return TreeBuilder.class.cast(clazz.newInstance());
                    } else {
                        throw new ELException("Builder " + clazz + " is missing constructor (can't pass features)");
                    }
                } else {
                    return TreeBuilder.class.cast(constructor.newInstance((Object) features));
                }
            } else {
                return TreeBuilder.class.cast(clazz.newInstance());
            }
        } catch (Exception e) {
            throw new ELException("TreeBuilder " + clazz + " could not be instantiated", e);
        }
    }

    private Class<?> load(Class<?> clazz, Properties properties) {
        if (properties != null) {
            String className = properties.getProperty(clazz.getName());
            if (className != null) {
                ClassLoader loader;
                try {
                    loader = Thread.currentThread().getContextClassLoader();
                } catch (Exception e) {
                    throw new ELException("Could not get context class loader", e);
                }
                try {
                    return loader == null ? Class.forName(className) : loader.loadClass(className);
                } catch (ClassNotFoundException e) {
                    throw new ELException("Class " + className + " not found", e);
                } catch (Exception e) {
                    throw new ELException("Class " + className + " could not be instantiated", e);
                }
            }
        }
        return null;
    }

    @Override
    public final Object coerceToType(Object obj, Class<?> targetType) {
        return converter.convert(obj, targetType);
    }

    @Override
    public final ObjectValueExpression createValueExpression(Object instance, Class<?> expectedType) {
        return new ObjectValueExpression(converter, instance, expectedType);
    }

    @Override
    public final TreeValueExpression createValueExpression(ELContext context, String expression, Class<?> expectedType) {
        return new TreeValueExpression(store, context.getFunctionMapper(), context.getVariableMapper(), converter,
                expression, expectedType);
    }

    @Override
    public final TreeMethodExpression createMethodExpression(ELContext context, String expression,
                                                             Class<?> expectedReturnType, Class<?>[] expectedParamTypes) {
        return new TreeMethodExpression(store, context.getFunctionMapper(), context.getVariableMapper(), converter,
                expression, expectedReturnType, expectedParamTypes);
    }
}
